قدرت پیمایش (iteration) پایتون را آزاد کنید. راهنمای جامع برای توسعهدهندگان جهت پیادهسازی ایتریتورهای سفارشی با استفاده از متدهای __iter__ و __next__ همراه با مثالهای عملی.
ابهامزدایی از پروتکل Iterator پایتون: نگاهی عمیق به __iter__ و __next__
پیمایش (Iteration) یکی از اساسیترین مفاهیم در برنامهنویسی است. در پایتون، این مکانیزم زیبا و کارآمد، نیروی محرکهی همه چیز، از حلقههای ساده for گرفته تا خطوط لوله پردازش دادههای پیچیده است. شما هر روز هنگام حلقه زدن روی یک لیست، خواندن خطوط از یک فایل یا کار با نتایج پایگاه داده از آن استفاده میکنید. اما آیا تا به حال فکر کردهاید که در پشت صحنه چه اتفاقی میافتد؟ چگونه پایتون میداند که چطور آیتم «بعدی» را از این همه نوع شیء متفاوت دریافت کند؟
پاسخ در یک الگوی طراحی قدرتمند و زیبا به نام پروتکل Iterator نهفته است. این پروتکل، زبان مشترکی است که تمام اشیاء شبهترتیبی (sequence-like) پایتون با آن صحبت میکنند. با درک و پیادهسازی این پروتکل، شما میتوانید اشیاء سفارشی خود را بسازید که کاملاً با ابزارهای پیمایش پایتون سازگار باشند و کد شما را گویاتر، بهینهتر از نظر حافظه و اساساً «پایتونیک» (Pythonic) کنند.
این راهنمای جامع شما را به سفری عمیق به درون پروتکل ایتریتور میبرد. ما جادوی پشت متدهای `__iter__` و `__next__` را رمزگشایی خواهیم کرد، تفاوت حیاتی بین یک شیء پیمایشپذیر (iterable) و یک ایتریتور (iterator) را روشن خواهیم ساخت، و شما را در ساخت ایتریتورهای سفارشی خود از ابتدا راهنمایی خواهیم کرد. چه یک توسعهدهنده سطح متوسط باشید که به دنبال تعمیق درک خود از سازوکارهای داخلی پایتون است، و چه یک متخصص که قصد طراحی APIهای پیچیدهتر را دارد، تسلط بر پروتکل ایتریتور یک گام حیاتی در سفر شماست.
«چرا»ی ماجرا: اهمیت و قدرت پیمایش
قبل از اینکه به پیادهسازی فنی بپردازیم، ضروری است که قدردان اهمیت پروتکل ایتریتور باشیم. مزایای آن بسیار فراتر از فعال کردن حلقههای `for` است.
بهینگی حافظه و ارزیابی تنبل (Lazy Evaluation)
تصور کنید نیاز به پردازش یک فایل لاگ عظیم دارید که چندین گیگابایت حجم دارد. اگر بخواهید کل فایل را در یک لیست در حافظه بخوانید، به احتمال زیاد منابع سیستم خود را تمام خواهید کرد. ایتریتورها این مشکل را به زیبایی از طریق مفهومی به نام ارزیابی تنبل حل میکنند.
یک ایتریتور تمام دادهها را به یکباره بارگذاری نمیکند. در عوض، هر بار فقط یک آیتم را، تنها زمانی که درخواست میشود، تولید یا واکشی میکند. ایتریتور یک وضعیت داخلی را برای به خاطر سپردن موقعیت خود در توالی حفظ میکند. این بدان معناست که شما میتوانید یک جریان داده بینهایت بزرگ (در تئوری) را با مقدار بسیار کم و ثابتی از حافظه پردازش کنید. این همان اصلی است که به شما امکان میدهد یک فایل عظیم را خط به خط بدون از کار افتادن برنامهتان بخوانید.
کد تمیز، خوانا و جهانی
پروتکل ایتریتور یک رابط جهانی برای دسترسی ترتیبی فراهم میکند. از آنجا که لیستها، تاپلها، دیکشنریها، رشتهها، اشیاء فایل و بسیاری از انواع دیگر همگی از این پروتکل پیروی میکنند، میتوانید از همان سینتکس — حلقه `for` — برای کار با همه آنها استفاده کنید. این یکنواختی، سنگ بنای خوانایی پایتون است.
این کد را در نظر بگیرید:
کد:
my_list = [1, 2, 3]
for item in my_list:
print(item)
my_string = "abc"
for char in my_string:
print(char)
with open('my_file.txt', 'r') as f:
for line in f:
print(line)
حلقه `for` اهمیتی نمیدهد که آیا روی لیستی از اعداد صحیح، رشتهای از کاراکترها یا خطوطی از یک فایل پیمایش میکند. این حلقه به سادگی از شیء، ایتریتور خود را درخواست میکند و سپس به طور مکرر از آن ایتریتور، آیتم بعدیاش را میخواهد. این انتزاع (abstraction) فوقالعاده قدرتمند است.
کالبدشکافی پروتکل Iterator
خودِ پروتکل به طرز شگفتآوری ساده است و تنها با دو متد ویژه که اغلب متدهای «داندر» (dunder - مخفف double underscore) نامیده میشوند، تعریف میشود:
- `__iter__()`
- `__next__()`
برای درک کامل این دو، ابتدا باید تفاوت بین دو مفهوم مرتبط اما متفاوت را درک کنیم: یک شیء پیمایشپذیر (iterable) و یک ایتریتور (iterator).
شیء پیمایشپذیر در مقابل ایتریتور: یک تمایز حیاتی
این اغلب یک نقطه سردرگمی برای تازهکاران است، اما تفاوت آن حیاتی است.
شیء پیمایشپذیر (Iterable) چیست؟
یک شیء پیمایشپذیر (iterable) هر شیئی است که بتوان روی آن حلقه زد. این شیئی است که میتوانید آن را به تابع داخلی `iter()` پاس دهید تا یک ایتریتور دریافت کنید. از نظر فنی، یک شیء در صورتی پیمایشپذیر محسوب میشود که متد `__iter__` را پیادهسازی کرده باشد. تنها هدف متد `__iter__` آن بازگرداندن یک شیء ایتریتور است.
نمونههایی از اشیاء پیمایشپذیر داخلی عبارتند از:
- لیستها (`[1, 2, 3]`)
- تاپلها (`(1, 2, 3)`)
- رشتهها (`"hello"`)
- دیکشنریها (`{'a': 1, 'b': 2}` - روی کلیدها پیمایش میکند)
- مجموعهها (`{1, 2, 3}`)
- اشیاء فایل
شما میتوانید یک شیء پیمایشپذیر را به عنوان یک ظرف (container) یا منبع داده در نظر بگیرید. این شیء خودش نمیداند چگونه آیتمها را تولید کند، اما میداند چگونه شیئی را بسازد که این کار را انجام دهد: یعنی ایتریتور.
ایتریتور (Iterator) چیست؟
یک ایتریتور شیئی است که در واقع کار تولید مقادیر را در طول پیمایش انجام میدهد. این شیء نماینده جریانی از دادهها است. یک ایتریتور باید دو متد را پیادهسازی کند:
- `__iter__()`: این متد باید خود شیء ایتریتور (`self`) را برگرداند. این کار لازم است تا ایتریتورها بتوانند در جایی که اشیاء پیمایشپذیر انتظار میروند نیز استفاده شوند، به عنوان مثال، در یک حلقه `for`.
- `__next__()`: این متد موتور محرکهی ایتریتور است. این متد آیتم بعدی در توالی را برمیگرداند. وقتی آیتم دیگری برای بازگرداندن وجود نداشته باشد، باید استثنای `StopIteration` را ایجاد (raise) کند. این استثنا یک خطا نیست؛ بلکه سیگنال استانداردی به ساختار حلقه است که پیمایش به پایان رسیده است.
ویژگیهای کلیدی یک ایتریتور عبارتند از:
- وضعیت را حفظ میکند: یک ایتریتور موقعیت فعلی خود را در توالی به خاطر میسپارد.
- مقادیر را یکی یکی تولید میکند: از طریق متد `__next__`.
- تمامشدنی است: هنگامی که یک ایتریتور به طور کامل مصرف شد (یعنی `StopIteration` را ایجاد کرد)، خالی میشود. شما نمیتوانید آن را بازنشانی یا دوباره استفاده کنید. برای پیمایش مجدد، باید به شیء پیمایشپذیر اصلی برگردید و با فراخوانی مجدد `iter()` روی آن، یک ایتریتور جدید دریافت کنید.
ساخت اولین ایتریتور سفارشی ما: راهنمای گام به گام
تئوری عالی است، اما بهترین راه برای درک پروتکل، ساختن آن توسط خودتان است. بیایید یک کلاس ساده بسازیم که به عنوان یک شمارنده عمل میکند و از یک عدد شروع تا یک حد معین پیمایش میکند.
مثال ۱: یک کلاس شمارنده ساده
ما کلاسی به نام `CountUpTo` ایجاد خواهیم کرد. وقتی نمونهای از آن را ایجاد میکنید، یک عدد حداکثر مشخص میکنید و وقتی روی آن پیمایش میکنید، اعدادی از ۱ تا آن حداکثر را تولید میکند.
کد:
class CountUpTo:
"""An iterator that counts from 1 up to a specified maximum number."""
def __init__(self, max_num):
print("Initializing the CountUpTo object...")
self.max_num = max_num
self.current = 0 # This will store the state
def __iter__(self):
print("__iter__ called, returning self...")
# This object is its own iterator, so we return self
return self
def __next__(self):
print("__next__ called...")
if self.current < self.max_num:
self.current += 1
return self.current
else:
# This is the crucial part: signal that we are done.
print("Raising StopIteration.")
raise StopIteration
# How to use it
print("Creating the counter object...")
counter = CountUpTo(3)
print("\nStarting the for loop...")
for number in counter:
print(f"For loop received: {number}")
تجزیه و تحلیل کد و توضیحات
بیایید تحلیل کنیم که وقتی حلقه `for` اجرا میشود چه اتفاقی میافتد:
- مقداردهی اولیه: `counter = CountUpTo(3)` یک نمونه از کلاس ما ایجاد میکند. متد `__init__` اجرا شده، `self.max_num` را برابر ۳ و `self.current` را برابر ۰ قرار میدهد. وضعیت شیء ما اکنون مقداردهی اولیه شده است.
- شروع حلقه: وقتی به خط `for number in counter:` میرسیم، پایتون به صورت داخلی `iter(counter)` را فراخوانی میکند.
- `__iter__` فراخوانی میشود: فراخوانی `iter(counter)` متد `counter.__iter__()` ما را فرا میخواند. همانطور که از کد ما میبینید، این متد به سادگی یک پیام چاپ میکند و `self` را برمیگرداند. این به حلقه `for` میگوید: «شیئی که باید `__next__` را روی آن فراخوانی کنی، خود من هستم!»
- حلقه آغاز میشود: اکنون حلقه `for` آماده است. در هر تکرار، `next()` را روی شیء ایتریتوری که دریافت کرده (که همان شیء `counter` ماست) فراخوانی میکند.
- اولین فراخوانی `__next__`: متد `counter.__next__()` فراخوانی میشود. `self.current` برابر ۰ است که کمتر از `self.max_num` (۳) است. کد `self.current` را به ۱ افزایش داده و آن را برمیگرداند. حلقه `for` این مقدار را به متغیر `number` اختصاص میدهد و بدنه حلقه (`print(...)`) اجرا میشود.
- دومین فراخوانی `__next__`: حلقه ادامه مییابد. `__next__` دوباره فراخوانی میشود. `self.current` برابر ۱ است. به ۲ افزایش یافته و برگردانده میشود.
- سومین فراخوانی `__next__`: `__next__` دوباره فراخوانی میشود. `self.current` برابر ۲ است. به ۳ افزایش یافته و برگردانده میشود.
- فراخوانی نهایی `__next__`: `__next__` یک بار دیگر فراخوانی میشود. اکنون، `self.current` برابر ۳ است. شرط `self.current < self.max_num` نادرست است. بلوک `else` اجرا شده و `StopIteration` ایجاد میشود.
- پایان حلقه: حلقه `for` طوری طراحی شده است که استثنای `StopIteration` را بگیرد (catch). وقتی این کار را میکند، میفهمد که پیمایش به پایان رسیده و به آرامی خاتمه مییابد. برنامه به اجرای کدی که بعد از حلقه قرار دارد ادامه میدهد.
به یک نکته کلیدی توجه کنید: اگر سعی کنید حلقه `for` را دوباره روی همان شیء `counter` اجرا کنید، کار نخواهد کرد. ایتریتور تمام شده است. `self.current` از قبل ۳ است، بنابراین هر فراخوانی بعدی `__next__` بلافاصله `StopIteration` را ایجاد خواهد کرد. این نتیجه این است که شیء ما ایتریتور خودش است.
مفاهیم پیشرفته ایتریتور و کاربردهای دنیای واقعی
شمارندههای ساده راهی عالی برای یادگیری هستند، اما قدرت واقعی پروتکل ایتریتور زمانی میدرخشد که روی ساختارهای داده پیچیدهتر و سفارشی اعمال شود.
مشکل ترکیب شیء پیمایشپذیر و ایتریتور
در مثال `CountUpTo` ما، کلاس هم شیء پیمایشپذیر و هم ایتریتور بود. این ساده است اما یک اشکال عمده دارد: ایتریتور حاصل تمامشدنی (exhaustible) است. وقتی یک بار روی آن حلقه زدید، کارش تمام است.
کد:
counter = CountUpTo(2)
print("First iteration:")
for num in counter: print(num) # Works fine
print("\nSecond iteration:")
for num in counter: print(num) # Prints nothing!
این اتفاق میافتد زیرا وضعیت (`self.current`) روی خود شیء ذخیره میشود. پس از حلقه اول، `self.current` برابر ۲ است و هر فراخوانی بعدی `__next__` فقط `StopIteration` را ایجاد میکند. این رفتار با یک لیست استاندارد پایتون که میتوانید چندین بار روی آن پیمایش کنید، متفاوت است.
الگوی قویتر: جدا کردن شیء پیمایشپذیر از ایتریتور
برای ایجاد اشیاء پیمایشپذیر قابل استفاده مجدد مانند کالکشنهای داخلی پایتون، بهترین روش جدا کردن این دو نقش است. شیء ظرف (container) شیء پیمایشپذیر خواهد بود و هر بار که متد `__iter__` آن فراخوانی شود، یک شیء ایتریتور جدید و تازه تولید خواهد کرد.
بیایید مثال خود را به دو کلاس بازنویسی کنیم: `Sentence` (شیء پیمایشپذیر) و `SentenceIterator` (ایتریتور).
کد:
class SentenceIterator:
"""The iterator responsible for state and producing values."""
def __init__(self, words):
self.words = words
self.index = 0
def __next__(self):
try:
word = self.words[self.index]
except IndexError:
raise StopIteration()
self.index += 1
return word
def __iter__(self):
# An iterator must also be an iterable, returning itself.
return self
class Sentence:
"""The iterable container class."""
def __init__(self, text):
# The container holds the data.
self.words = text.split()
def __iter__(self):
# Each time __iter__ is called, it creates a NEW iterator object.
return SentenceIterator(self.words)
# How to use it
my_sentence = Sentence('This is a test')
print("First iteration:")
for word in my_sentence:
print(word)
print("\nSecond iteration:")
for word in my_sentence:
print(word)
حالا، دقیقاً مانند یک لیست کار میکند! هر بار که حلقه `for` شروع میشود، `my_sentence.__iter__()` را فراخوانی میکند، که یک نمونه کاملاً جدید `SentenceIterator` با وضعیت خاص خود (`self.index = 0`) ایجاد میکند. این امکان پیمایشهای متعدد و مستقل روی همان شیء `Sentence` را فراهم میکند. این الگو بسیار قویتر است و نحوه پیادهسازی کالکشنهای خود پایتون نیز به همین شکل است.
مثال: ایتریتورهای بینهایت
ایتریتورها نیازی به محدود بودن ندارند. آنها میتوانند یک توالی بیپایان از دادهها را نمایش دهند. اینجاست که ماهیت تنبل و یکی یکی آنها یک مزیت بزرگ است. بیایید یک ایتریتور برای یک توالی بینهایت از اعداد فیبوناچی ایجاد کنیم.
کد:
class FibonacciIterator:
"""Generates an infinite sequence of Fibonacci numbers."""
def __init__(self):
self.a, self.b = 0, 1
def __iter__(self):
return self
def __next__(self):
result = self.a
self.a, self.b = self.b, self.a + self.b
return result
# How to use it - CAUTION: Infinite loop without a break!
fib_gen = FibonacciIterator()
for i, num in enumerate(fib_gen):
print(f"Fibonacci({i}): {num}")
if i >= 10: # We must provide a stopping condition
break
این ایتریتور هرگز به خودی خود `StopIteration` را ایجاد نخواهد کرد. این مسئولیت کد فراخوان است که شرطی (مانند دستور `break`) برای خاتمه دادن به حلقه فراهم کند. این الگو در جریانسازی دادهها (data streaming)، حلقههای رویداد (event loops) و شبیهسازیهای عددی رایج است.
پروتکل Iterator در اکوسیستم پایتون
درک `__iter__` و `__next__` به شما امکان میدهد تأثیر آنها را در همه جای پایتون ببینید. این پروتکل یکپارچهکنندهای است که باعث میشود بسیاری از ویژگیهای پایتون به طور یکپارچه با هم کار کنند.
حلقههای `for` *واقعاً* چگونه کار میکنند
ما به طور ضمنی در این مورد بحث کردهایم، اما بیایید آن را صریح بیان کنیم. وقتی پایتون با این خط مواجه میشود:
`for item in my_iterable:`
این مراحل را در پشت صحنه انجام میدهد:
- `iter(my_iterable)` را فراخوانی میکند تا یک ایتریتور بگیرد. این به نوبه خود `my_iterable.__iter__()` را فراخوانی میکند. بیایید شیء بازگشتی را `iterator_obj` بنامیم.
- وارد یک حلقه بینهایت `while True` میشود.
- در داخل حلقه، `next(iterator_obj)` را فراخوانی میکند، که به نوبه خود `iterator_obj.__next__()` را فراخوانی میکند.
- اگر `__next__` مقداری را برگرداند، آن به متغیر `item` اختصاص داده میشود و کد داخل بلوک حلقه `for` اجرا میشود.
- اگر `__next__` یک استثنای `StopIteration` ایجاد کند، حلقه `for` این استثنا را گرفته و از حلقه `while` داخلی خود خارج میشود. پیمایش کامل شده است.
Comprehensionها و عبارات ژنراتور
List، set و dictionary comprehension همگی توسط پروتکل ایتریتور قدرت گرفتهاند. وقتی مینویسید:
`squares = [x * x for x in range(10)]`
پایتون در واقع در حال انجام یک پیمایش روی شیء `range(10)` است، هر مقدار را گرفته و عبارت `x * x` را برای ساخت لیست اجرا میکند. همین امر در مورد عبارات ژنراتور (generator expressions) نیز صادق است، که استفاده مستقیمتری از پیمایش تنبل هستند:
`lazy_squares = (x * x for x in range(1000000))`
این یک لیست یک میلیون آیتمی در حافظه ایجاد نمیکند. این یک ایتریتور (به طور خاص، یک شیء ژنراتور) ایجاد میکند که مربعها را یکی یکی، همانطور که روی آن پیمایش میکنید، محاسبه خواهد کرد.
ژنراتورها: راه سادهتر برای ایجاد ایتریتورها
در حالی که ایجاد یک کلاس کامل با `__iter__` و `__next__` به شما حداکثر کنترل را میدهد، میتواند برای موارد ساده پرحرف باشد. پایتون یک سینتکس بسیار مختصرتر برای ایجاد ایتریتورها فراهم میکند: ژنراتورها.
یک ژنراتور تابعی است که از کلمه کلیدی `yield` استفاده میکند. وقتی یک تابع ژنراتور را فراخوانی میکنید، کد را اجرا نمیکند. در عوض، یک شیء ژنراتور برمیگرداند که یک ایتریتور تمام عیار است.
بیایید مثال `CountUpTo` خود را به عنوان یک ژنراتور بازنویسی کنیم:
کد:
def count_up_to_generator(max_num):
"""A generator function that yields numbers from 1 to max_num."""
print("Generator started...")
current = 1
while current <= max_num:
yield current # Pauses here and sends a value back
current += 1
print("Generator finished.")
# How to use it
counter_gen = count_up_to_generator(3)
for number in counter_gen:
print(f"For loop received: {number}")
ببینید چقدر سادهتر است! کلمه کلیدی `yield` جادوی اینجاست. وقتی با `yield` مواجه میشویم، وضعیت تابع منجمد میشود، مقدار به فراخواننده ارسال میشود و تابع متوقف میشود. دفعه بعد که `__next__` روی شیء ژنراتور فراخوانی شود، تابع اجرا را دقیقاً از همان جایی که متوقف شده بود از سر میگیرد، تا زمانی که به `yield` دیگری برخورد کند یا تابع به پایان برسد. وقتی تابع تمام میشود، یک `StopIteration` به طور خودکار برای شما ایجاد میشود.
در پشت صحنه، پایتون به طور خودکار یک شیء با متدهای `__iter__` و `__next__` ایجاد کرده است. در حالی که ژنراتورها اغلب انتخاب عملیتری هستند، درک پروتکل زیربنایی برای اشکالزدایی، طراحی سیستمهای پیچیده و درک نحوه کار مکانیکهای اصلی پایتون ضروری است.
بهترین شیوهها و دامهای متداول
هنگام پیادهسازی پروتکل ایتریتور، این دستورالعملها را برای جلوگیری از خطاهای رایج در ذهن داشته باشید.
بهترین شیوهها
- جدا کردن شیء پیمایشپذیر و ایتریتور: برای هر شیء ظرفی که باید از پیمایشهای متعدد پشتیبانی کند، همیشه ایتریتور را در یک کلاس جداگانه پیادهسازی کنید. متد `__iter__` ظرف باید هر بار یک نمونه جدید از کلاس ایتریتور را برگرداند.
- همیشه `StopIteration` را ایجاد کنید: متد `__next__` باید به طور قابل اعتمادی `StopIteration` را برای سیگنال دادن به پایان ایجاد کند. فراموش کردن این موضوع منجر به حلقههای بینهایت خواهد شد.
- ایتریتورها باید پیمایشپذیر باشند: متد `__iter__` یک ایتریتور باید همیشه `self` را برگرداند. این اجازه میدهد تا یک ایتریتور در هر جایی که یک شیء پیمایشپذیر انتظار میرود استفاده شود.
- برای سادگی، ژنراتورها را ترجیح دهید: اگر منطق ایتریتور شما سرراست است و میتواند به عنوان یک تابع واحد بیان شود، یک ژنراتور تقریباً همیشه تمیزتر و خواناتر است. از یک کلاس ایتریتور کامل زمانی استفاده کنید که نیاز به مرتبط کردن وضعیت یا متدهای پیچیدهتر با خود شیء ایتریتور دارید.
دامهای متداول
- مشکل ایتریتور تمامشدنی: همانطور که بحث شد، آگاه باشید که وقتی یک شیء ایتریتور خودش است، فقط یک بار قابل استفاده است. اگر نیاز به پیمایش چندین باره دارید، باید یا یک نمونه جدید ایجاد کنید یا از الگوی جداگانه شیء پیمایشپذیر/ایتریتور استفاده کنید.
- فراموش کردن وضعیت: متد `__next__` باید وضعیت داخلی ایتریتور را تغییر دهد (مثلاً افزایش یک شاخص یا پیش بردن یک اشارهگر). اگر وضعیت بهروز نشود، `__next__` بارها و بارها همان مقدار را برمیگرداند و احتمالاً باعث یک حلقه بینهایت میشود.
- تغییر یک کالکشن حین پیمایش: پیمایش روی یک کالکشن در حالی که آن را تغییر میدهید (مثلاً حذف آیتمها از یک لیست در داخل حلقه `for` که روی آن پیمایش میکند) میتواند منجر به رفتار غیرقابل پیشبینی، مانند رد کردن آیتمها یا ایجاد خطاهای غیرمنتظره شود. به طور کلی امنتر است که اگر نیاز به تغییر نسخه اصلی دارید، روی یک کپی از کالکشن پیمایش کنید.
نتیجهگیری
پروتکل ایتریتور، با متدهای ساده `__iter__` و `__next__` خود، سنگ بنای پیمایش در پایتون است. این گواهی بر فلسفه طراحی این زبان است: ترجیح دادن رابطهای ساده و سازگار که رفتارهای قدرتمند و پیچیده را امکانپذیر میسازند. با فراهم کردن یک قرارداد جهانی برای دسترسی به دادههای ترتیبی، این پروتکل به حلقههای `for`، comprehensionها و ابزارهای بیشمار دیگر اجازه میدهد تا به طور یکپارچه با هر شیئی که زبان آن را صحبت میکند، کار کنند.
با تسلط بر این پروتکل، شما توانایی ایجاد اشیاء شبهترتیبی خود را که شهروندان درجه یک در اکوسیستم پایتون هستند، باز کردهاید. اکنون میتوانید کلاسهایی بنویسید که با پردازش تنبل دادهها از نظر حافظه بهینهتر هستند، با ادغام تمیز با سینتکس استاندارد پایتون شهودیتر هستند و در نهایت، قدرتمندتر هستند. دفعه بعد که یک حلقه `for` مینویسید، لحظهای را به قدردانی از رقص زیبای `__iter__` و `__next__` که درست در زیر سطح اتفاق میافتد، اختصاص دهید.